package controllers

import (
    "gouniapp/configs"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"crypto/hmac"
	"fmt"
	"log"
	"io"
	"net/http"
	"net/url"
	"mime"
	"os"
	"path/filepath"
	"strings"
	"errors"
	"runtime"
	"time"
	"github.com/gin-gonic/gin"
)

const serveurl = "https://zp.kzhgzs.cn/v1/"
// 文件元数据结构
type FileMeta struct {
	Path     string `json:"path"`
	Hash     string `json:"hash"`  // SHA256哈希
	Size     int64  `json:"size"`
	IsBinary bool   `json:"isBinary"`
	Action   string `json:"action,omitempty"` // 操作类型：新增，修改，删除
}

// 差异分析响应
type DiffResponse struct {
	NeedUpdate bool       `json:"needUpdate"`
	Files      []FileMeta `json:"files"`
}

// 受保护文件
var protectedFiles = []string{
    "config.yaml",
    "permissions.yaml",
    "templates/pc/static/menus.js",
    "templates/static/h5config.js",
    "gouniapp.zip",
    "temp/",
    ".git/",
    ".gitignore",
    ".env",
    "uploads/",
}

// 客户端的 GenerateDownloadToken
func GenerateDownloadToken(hwID string) string {
    timestamp := time.Now().Unix() / 300
    raw := fmt.Sprintf("%s|%d|300", hwID, timestamp) // 确保格式正确
    h := hmac.New(sha256.New, []byte(configs.Jmkey))
    h.Write([]byte(raw))
    token := hex.EncodeToString(h.Sum(nil))
    log.Printf("[Client] 生成令牌: %s", token)
    return token
}

// 通用请求方法（带认证）
func doAuthRequest(method, url string, body io.Reader) (*http.Response, error) {
    hardwareID := configs.Getsysid()                     // 获取硬件ID
    clientSN := configs.Config.GetString("server.sn")    // 序列号
    log.Printf("HWID: %s | SN: %s", hardwareID, clientSN)
    // 1. 创建请求并处理错误
    req, err := http.NewRequest(method, url, body)
    if err != nil {
        return nil, fmt.Errorf("创建请求失败: %w", err)
    }
    // 2. 添加认证头
    req.Header.Set("X-HWID", hardwareID)
    req.Header.Set("X-SN", clientSN)
    // 3. 生成下载令牌（仅GET需要）
    token := GenerateDownloadToken(hardwareID)
    log.Printf("生成令牌: %s", token)
    if method == http.MethodGet {
        q := req.URL.Query()
        q.Add("t", token)
        req.URL.RawQuery = q.Encode()
    }
    // 4. 创建带超时的Client
    client := &http.Client{
        Timeout: 30 * time.Minute,
    }
    // 5. 使用自定义Client发送请求
    return client.Do(req)
}

// 检查更新接口
func CheckUpdate(c *gin.Context) {
	remoteFiles, err := fetchRemoteFileList() // 从源站获取最新文件列表
	if err != nil {
	    fmt.Println(err)
        // 添加特定错误处理
        if strings.Contains(err.Error(), "403") {
            JsonResponse(c, http.StatusForbidden, "客户端未授权", nil)
            return
        }
	    JsonResponse(c, http.StatusInternalServerError, "获取远程文件列表失败", err)
		return
	}
	localFiles, errorList := getFileList() // 获取本地文件列表
    if len(errorList) > 0 {
        JsonResponse(c, http.StatusInternalServerError, "获取本地文件列表失败", nil)
		return
	}
    fileMap := make(map[string]FileMeta)
    for _, f := range localFiles {
        fileMap[f.Path] = f
    }
	diff := compareFiles(fileMap, remoteFiles) 	// 分析差异
	if len(diff) == 0 {
		JsonResponse(c, http.StatusBadRequest, "无需更新", nil)
		return
	}
	JsonResponse(c, http.StatusOK, "获取成功", diff)
}

// 获取远程文件列表
func fetchRemoteFileList() (map[string]FileMeta, error) {
	resp, err := doAuthRequest(http.MethodPost, serveurl+"version", nil)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
    if resp.StatusCode != http.StatusOK {
		return nil, errors.New("获取更新文件列表失败: " + resp.Status)
	}
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	var files []FileMeta
	if err := json.Unmarshal(body, &files); err != nil {
		return nil, err
	}
	fileMap := make(map[string]FileMeta)
	for _, f := range files {
		fileMap[f.Path] = f
	}
	return fileMap, nil
}

// 文件差异对比
func compareFiles(local, remote map[string]FileMeta) []FileMeta {
    var diff []FileMeta
    // 检测新增和修改
    for rPath, rFile := range remote {
        lFile, exists := local[rPath]
        if !exists {
            // 新增文件
            rFile.Action = "create"
            diff = append(diff, rFile)
        } else if lFile.Hash != rFile.Hash {
            // 修改文件
            rFile.Action = "update"
            diff = append(diff, rFile)
        }
    }
    // 检测需要删除的文件
    for lPath := range local {
        if _, exists := remote[lPath]; !exists {
            // 标记删除
            diff = append(diff, FileMeta{
                Path:   lPath,
                Action: "delete",
            })
        }
    }
    return diff
}

// 更新文件接口
func UpdateFiles(c *gin.Context) {
    var files []FileMeta
    if err := c.BindJSON(&files); err != nil {
        JsonResponse(c, http.StatusBadRequest, "无效请求", nil)
        return
    }
    // 创建临时目录
    tmpDir, _ := os.MkdirTemp("", "update-*")
    defer os.RemoveAll(tmpDir)
    // 下载需要更新的文件（排除删除操作）
    for _, file := range files {
        if file.Action == "delete" {
            continue // 删除操作不需要下载文件
        }
        if err := downloadFile(file.Path, tmpDir, file.Hash); err != nil {
            log.Printf("文件下载失败: %s 错误: %v", file.Path, err)
            // 捕获认证错误
            if strings.Contains(err.Error(), "403") {
                JsonResponse(c, http.StatusForbidden, "授权失效，请重新验证", nil)
                return
            }
            JsonResponse(c, http.StatusInternalServerError, "文件下载失败: "+file.Path, nil)
            return
        }
    }
    // 应用更新
    if err := applyUpdate(tmpDir, files); err != nil {
        log.Printf("应用更新失败: %v", err)
        JsonResponse(c, http.StatusInternalServerError, "应用更新失败", gin.H{
            "error": err.Error(),
        })
        return
    }
    JsonResponse(c, http.StatusOK, "更新完成，需要重启生效", gin.H{
        "restart_required": true,
    })
}

// 安全删除文件
func safeDelete(path string) error {
    cleanPath := filepath.Clean(path)
    for _, p := range protectedFiles {
        if strings.Contains(cleanPath, p) {
            return fmt.Errorf("拒绝删除受保护文件: %s", path)
        }
    }
    // 检查文件是否存在
    if _, err := os.Stat(path); os.IsNotExist(err) {
        return nil // 文件不存在直接返回成功
    }
    // 实际删除操作
    return os.Remove(path)
}

// 应用更新（包含删除处理）
func applyUpdate(tmpDir string, diff []FileMeta) error {
    // 先处理删除操作
    for _, file := range diff {
        if file.Action == "delete" {
            targetPath := filepath.Join(file.Path)
            if err := safeDelete(targetPath); err != nil {
                return fmt.Errorf("删除失败 %s: %w", file.Path, err)
            }
        }
    }
    // 处理新增/修改操作
    return filepath.Walk(tmpDir, func(srcPath string, info os.FileInfo, err error) error {
        if err != nil || info.IsDir() {
            return nil
        }
        // 获取相对路径
        relPath, _ := filepath.Rel(tmpDir, srcPath)
        destPath := filepath.Join(relPath)
        // 跳过受保护文件
        if isProtectedFile(destPath) {
            return nil
        }
        // 使用复制代替移动（解决Windows文件占用问题）
        if err := copyFile(srcPath, destPath); err != nil {
            return fmt.Errorf("复制文件失败 %s: %w", relPath, err)
        }
        return nil
    })
}

// 跨平台文件复制
func copyFile(src, dst string) error {
    if _, err := os.Stat(dst); err == nil {
        if err := os.Remove(dst); err != nil && !os.IsNotExist(err) {
            // Windows特殊处理：延迟重试
            if runtime.GOOS == "windows" {
                time.Sleep(500 * time.Millisecond)
                if err := os.Remove(dst); err != nil {
                    return fmt.Errorf("无法删除被占用的文件: %w", err)
                }
            } else {
                return err
            }
        }
    }
    // 确保目标目录存在
    if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
        return err
    }
    log.Printf("正在复制文件 源:%s -> 目标:%s", src, dst)
    // 打开源文件
    srcFile, err := os.Open(src)
    if err != nil {
        return err
    }
    defer srcFile.Close()
    // 创建目标文件
    dstFile, err := os.Create(dst)
    if err != nil {
        return err
    }
    defer dstFile.Close()
    // 复制内容
    if _, err = io.Copy(dstFile, srcFile); err != nil {
        return err
    }
    // 获取目标文件名（不含路径）
    fileName := filepath.Base(dst)
    // 仅对 gouniapp 或 gouniapp.exe 设置权限
    if fileName == "gouniapp" || fileName == "gouniapp.exe" {
        if err := os.Chmod(dst, 0755); err != nil {
            return fmt.Errorf("设置可执行权限失败: %w", err)
        }
        log.Printf("已为可执行文件设置权限: %s (0755)", dst)
    } else {
        // 其他文件保持源权限
        srcInfo, err := os.Stat(src)
        if err != nil {
            return err
        }
        if err := os.Chmod(dst, srcInfo.Mode()); err != nil {
            return fmt.Errorf("同步文件权限失败: %w", err)
        }
    }
    return nil
}

// 判断受保护文件
func isProtectedFile(path string) bool {
    cleanPath := filepath.Clean(path)
    for _, p := range protectedFiles {
        if strings.Contains(cleanPath, p) {
            return true
        }
    }
    return false
}

// 下载单个文件
func downloadFile(path, saveDir string, expectedHash string) error {
    log.Printf("正在下载文件 路径:%s 预期哈希:%s", path, expectedHash)
    // 构造正确下载URL（需与服务端路由匹配）
    encodedPath := url.PathEscape(path)
    downloadURL := serveurl + "download/" + encodedPath // 注意/file/路由需与服务端匹配
    log.Printf("下载地址: %s", downloadURL)
    resp, err := doAuthRequest(http.MethodGet, downloadURL, nil)
    if err != nil {
        return fmt.Errorf("请求失败: %w", err)
    }
    defer resp.Body.Close()
    // 检查HTTP状态码
    if resp.StatusCode != http.StatusOK {
        return fmt.Errorf("无效状态码: %d", resp.StatusCode)
    }
    // 创建目录结构
    fullPath := filepath.Join(saveDir, path)
    if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
        return fmt.Errorf("创建目录失败: %w", err)
    }
    // 创建临时文件（避免覆盖现有文件）
    tmpFile, err := os.CreateTemp(saveDir, "tmp-")
    if err != nil {
        return fmt.Errorf("创建临时文件失败: %w", err)
    }
    defer os.Remove(tmpFile.Name())
    // 同时进行下载和哈希计算
    hasher := sha256.New()
    teeReader := io.TeeReader(resp.Body, hasher)
    // 先写入文件
    if _, err := io.Copy(tmpFile, teeReader); err != nil {
        return fmt.Errorf("下载写入失败: %w", err)
    }
    tmpFile.Close() // 必须关闭才能重命名
    // 哈希校验
    actualHash := hex.EncodeToString(hasher.Sum(nil))
    if actualHash != expectedHash {
        return fmt.Errorf("哈希校验失败 预期:%s 实际:%s", expectedHash, actualHash)
    }
    // 原子操作：重命名临时文件到目标位置
    if err := os.Rename(tmpFile.Name(), fullPath); err != nil {
        return fmt.Errorf("文件替换失败: %w", err)
    }
    // 新增权限设置
    if err := os.Chmod(fullPath, 0755); err != nil {
        return fmt.Errorf("设置文件权限失败: %w", err)
    }
    log.Printf("下载成功: %s 大小: %d", fullPath, resp.ContentLength)
    return nil
}

// 安全路径拼接（防御路径遍历）
func secureJoin(base, subpath string) (string, error) {
    targetPath := filepath.Join(base, subpath)
    relPath, err := filepath.Rel(base, targetPath)
    if err != nil {
        return "", err
    }
    if strings.HasPrefix(relPath, "..") {
        return "", errors.New("非法路径访问")
    }
    return targetPath, nil
}

// 重启接口
func Restart(c *gin.Context) {
	go func() {
		// 等待1秒钟再重启，避免与请求处理冲突
		time.Sleep(1 * time.Second)
		os.Exit(0)
	}()
	JsonResponse(c, http.StatusOK, "重启指令已发送", nil)
}

//源码演示站提供相关的接口-----------------------
// 查找需要更新的文件
func SourceVersionHandler(c *gin.Context) {
    files, errorList := getFileList()
    if len(errorList) > 0 {
        c.JSON(http.StatusPartialContent, gin.H{
            "files": files,
            "errors": errorList,
        })
        return
    }
    c.JSON(http.StatusOK, files)
}

// 获取文件列表，排除config.yaml和templates/static/h5config.js
func getFileList() ([]FileMeta, []string) {
    type ScanTarget struct {
        Path     string
        IsBinary bool
        IsDir    bool
    }
    scanTargets := []ScanTarget{
        {Path: "gouniapp", IsBinary: true, IsDir: false},
        {Path: "templates", IsBinary: false, IsDir: true},
    }
    var files []FileMeta
    var errorList []string
    for _, target := range scanTargets {
        if target.IsDir {
            err := filepath.Walk(target.Path, func(path string, info os.FileInfo, err error) error {
                if err != nil || info.IsDir() || strings.Contains(path, "h5config.js") || strings.Contains(path, "menus.js") {
                    return nil
                }
                meta, err := getSourceFileMeta(path, target.IsBinary)
                if err != nil {
                    errorList = append(errorList, fmt.Sprintf("%s: %v", path, err))
                    return nil
                }
                meta.Path = filepath.ToSlash(filepath.Clean(path))
                files = append(files, meta)
                return nil
            })
            if err != nil {
                errorList = append(errorList, 
                    fmt.Sprintf("目录%s遍历失败: %v", target.Path, err))
            }
        } else {
            if _, err := os.Stat(target.Path); os.IsNotExist(err) {
                continue
            }
            meta, err := getSourceFileMeta(target.Path, target.IsBinary)
            if err != nil {
                errorList = append(errorList, fmt.Sprintf("文件%s扫描失败: %v", target.Path, err))
                continue
            }
            meta.Path = filepath.ToSlash(filepath.Clean(target.Path))
            files = append(files, meta)
        }
    }
    return files, errorList
}

// 源站专用文件元数据获取
func getSourceFileMeta(path string, isBinary bool) (FileMeta, error) {
    file, err := os.OpenFile(path, os.O_RDONLY, 0644)
    if err != nil {
        if os.IsPermission(err) {
            // Windows下特殊处理
            if runtime.GOOS == "windows" {
                // 创建临时副本
                tmpFile, err := os.CreateTemp("", "bin_copy_")
                if err != nil {
                    return FileMeta{}, err
                }
                defer os.Remove(tmpFile.Name())
                exe, _ := os.Executable()
                input, _ := os.Open(exe)
                defer input.Close()
                io.Copy(tmpFile, input)
                tmpFile.Close()
                path = tmpFile.Name() // 使用副本路径
            }
        } else {
            return FileMeta{}, err
        }
    }
    defer file.Close()
    // 计算SHA256哈希
    hasher := sha256.New()
    if _, err := io.Copy(hasher, file); err != nil {
        return FileMeta{}, err
    }
    fileStat, _ := file.Stat()
    return FileMeta{
        Path:     path,
        Hash:     hex.EncodeToString(hasher.Sum(nil)),
        Size:     fileStat.Size(),
        IsBinary: isBinary,
    }, nil
}

// 文件下载接口
func FileDownloadHandler(c *gin.Context) {
    // 步骤1: 解码路径
    requestedPath := c.Param("filepath")
    decodedPath, err := url.PathUnescape(requestedPath)
    if err != nil {
        log.Printf("路径解码失败: %s | 错误: %v", requestedPath, err)
        c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "路径编码错误"})
        return
    }
    // 步骤2: 获取并验证工作目录
    currentDir, err := os.Getwd()
    if err != nil {
        log.Printf("获取工作目录失败: %v", err)
        c.AbortWithStatusJSON(500, gin.H{"error": "系统初始化失败"})
        return
    }
    log.Printf("当前工作目录: %s | 原始请求路径: %s", currentDir, decodedPath)
    // 步骤3: 构建安全路径
    cleanPath := filepath.ToSlash(filepath.Clean(decodedPath))
    if strings.Contains(cleanPath, "..") {
        log.Printf("检测到路径遍历: %s", cleanPath)
        c.AbortWithStatusJSON(400, gin.H{"error": "路径包含非法字符"})
        return
    }
    fullPath := filepath.Join(currentDir, cleanPath)
    absFullPath, err := filepath.Abs(fullPath)
    if err != nil {
        log.Printf("绝对路径转换失败: %s", fullPath)
        c.AbortWithStatusJSON(400, gin.H{"error": "非法路径"})
        return
    }
    // 步骤4: 增强路径校验 允许访问的路径列表（运行目录自身 + templates子目录）
    allowedPaths := []string{
        filepath.ToSlash(filepath.Join(currentDir, "templates")) + "/",
        filepath.ToSlash(currentDir) + "/",  // 关键修改：允许运行目录下的文件
    }
    // 检查路径是否匹配任意允许前缀
    isAllowed := false
    absFullPathSlash := filepath.ToSlash(absFullPath)
    for _, prefix := range allowedPaths {
        if strings.HasPrefix(absFullPathSlash, prefix) {
            isAllowed = true
            break
        }
    }
    // 额外允许gouniapp文件（即使不在子目录）
    if filepath.Base(absFullPath) == "gouniapp" || filepath.Base(absFullPath) == "gouniapp.exe" {
        isAllowed = true
    }
    if !isAllowed {
        log.Printf("路径越界: 允许前缀%v | 请求路径[%s]", allowedPaths, absFullPathSlash)
        c.AbortWithStatusJSON(400, gin.H{"error": "非法文件路径请求"})
        return
    }
    // 步骤5: 文件检查
    fileInfo, err := os.Stat(absFullPath)
    if os.IsNotExist(err) {
        log.Printf("文件不存在: %s", absFullPath)
        c.AbortWithStatusJSON(404, gin.H{"error": "文件不存在"})
        return
    }
    // 文件类型检查（增强版）
    if isDangerousFile(absFullPath) {
        log.Printf("危险文件类型: %s", absFullPath)
        c.AbortWithStatusJSON(403, gin.H{"error": "禁止下载该类型文件"})
        return
    }
    // 步骤6: 准备下载
    file, err := os.Open(absFullPath)
    if err != nil {
        log.Printf("文件打开失败: %s | 错误: %v", absFullPath, err)
        c.AbortWithStatusJSON(500, gin.H{"error": "文件打开失败"})
        return
    }
    defer file.Close()
    // 设置安全头（Content-Type改进）
    mimeType := mime.TypeByExtension(filepath.Ext(absFullPath))
    if mimeType == "" {
        mimeType = "application/octet-stream"
    }
    c.Header("Content-Type", mimeType)
    c.Header("X-Content-Type-Options", "nosniff")
    c.Header("Content-Disposition", fmt.Sprintf(
        "attachment; filename=\"%s\"; filename*=UTF-8''%s",
        url.QueryEscape(filepath.Base(fullPath)),
        url.PathEscape(filepath.Base(fullPath)),
    ))
    c.Header("Cache-Control", "no-store, max-age=0")
    // 限速下载（每秒最大50MB）
    rateLimiter := time.Tick(time.Second / 50)
    file, err = os.Open(fullPath)
    if err != nil {
        c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
            "error": "文件打开失败",
        })
        return
    }
    defer file.Close()
    // 记录下载日志
    log.Printf("下载请求 - 客户端IP: %s | 文件: %s | 大小: %d", c.ClientIP(), cleanPath, fileInfo.Size())
    // 流式传输（带哈希校验）
    hasher := sha256.New()
    teeReader := io.TeeReader(file, hasher)
    // 使用CopyBuffer控制速率
    buf := make([]byte, 32*1024) // 32KB buffer
    for {
        <-rateLimiter
        n, err := teeReader.Read(buf)
        if n > 0 {
            if _, werr := c.Writer.Write(buf[:n]); werr != nil {
                break
            }
            c.Writer.Flush()
        }
        if errors.Is(err, io.EOF) {
            break
        }
        if err != nil {
            log.Printf("下载中断: %v", err)
            break
        }
    }
    // 记录下载校验
    log.Printf("下载完成 - 文件: %s | 校验和: %x", cleanPath, hasher.Sum(nil))
}

// 危险文件类型检查
func isDangerousFile(path string) bool {
    ext := strings.ToLower(filepath.Ext(path))
    dangerousExts := map[string]bool{
        ".zip":  true,
        ".rar":  true,
        ".yaml":  true,
        ".bat":  true,
        ".sh":   true,
        ".php":  true,
        ".dll":  true,
        ".so":   true,
        ".dylib": true,
    }
    return dangerousExts[ext]
}